// base HttpClass
import merge from 'lodash-es/merge'
import {
  createQuery,
  createMutation,
  getNameFromPayload,
  cacheBurst,
  urlParams,
  resultHandler
} from '../utils'
import {
  isObject,
  isString
} from 'jsonql-params-validator'
import {
  JsonqlValidationError,
  JsonqlServerError,
  clientErrorsHandler
} from 'jsonql-errors'
import {
  API_REQUEST_METHODS,
  DEFAULT_HEADER,
  JSONP_CALLBACK_NAME,
  SHOW_CONTRACT_DESC_PARAM
} from 'jsonql-constants'

// extract the one we need
const [ POST, PUT ] = API_REQUEST_METHODS

const _log = (...args) => {
  try {
    if (window && window.console) {
      Reflect.apply(console.log, null, args)
    }
  } catch(e) {}
}

export default class HttpClass {
  /**
   * The opts has been check at the init stage
   * @param {object} opts configuration options
   */
  constructor(opts) {
    // change the way how we init Fly
    // flyio now become external depedencies and it makes it easier to switch
    // @BUG should we run test to check if we have the windows object?
    _log(opts)
    this.fly = opts.Fly ? new opts.Fly() : new Fly()
    // to a different environment like WeChat mini app
    this.opts = opts;
    this.extraHeader = {};
    // @1.2.1 for adding query to the call on the fly
    this.extraParams = {};
    // this.log('start up opts', opts);
    this.reqInterceptor()
    this.resInterceptor()
  }

  // set headers for that one call
  set headers(header) {
    this.extraHeader = header;
  }

  /**
   * Create the reusage request method
   * @param {object} payload jsonql payload
   * @param {object} options extra options add the request
   * @param {object} headers extra headers add to the call
   * @return {object} the fly request instance
   */
  request(payload, options = {}, headers = {}) {
    this.headers = headers;
    let params = merge({}, cacheBurst(), this.extraParams)
    // @TODO need to add a jsonp url and payload
    if (this.opts.enableJsonp) {
      let resolverName = getNameFromPayload(payload)
      params = merge({}, params, {[JSONP_CALLBACK_NAME]: resolverName})
      payload = payload[resolverName]
    }
    return this.fly.request(
      this.jsonqlEndpoint,
      payload,
      merge({}, { method: POST, params }, options)
    )
  }

  /**
   * This will replace the create baseRequest method
   *
   */
  reqInterceptor() {
    this.fly.interceptors.request.use(
      req => {
        const headers = this.getHeaders();
        this.log('request interceptor call', headers)

        for (let key in headers) {
          req.headers[key] = headers[key];
        }
        return req;
      }
    )
  }

  // @TODO
  processJsonp(result) {
    return resultHandler(result)
  }

  /**
   * This will be replacement of the first then call
   *
   */
  resInterceptor() {
    const self = this;
    const jsonp = self.opts.enableJsonp;
    this.fly.interceptors.response.use(
      res => {
        this.log('response interceptor call')
        self.cleanUp()
        // now more processing here
        // there is a problem if we throw the result.error here
        // the original data is lost, so we need to do what we did before
        // deal with that error in the first then instead
        const result = isString(res.data) ? JSON.parse(res.data) : res.data;
        if (jsonp) {
          return self.processJsonp(result)
        }
        return resultHandler(result)
      },
      // this get call when it's not 200
      err => {
        self.cleanUp()
        console.error(err)
        throw new JsonqlServerError('Server side error', err)
      }
    )
  }

  /**
   * Get the headers inject into the call
   * @return {object} headers
   */
  getHeaders() {
    if (this.opts.enableAuth) {
      return merge({}, DEFAULT_HEADER, this.getAuthHeader(), this.extraHeader)
    }
    return merge({}, DEFAULT_HEADER, this.extraHeader)
  }

  /**
   * Post http call operation to clean up things we need
   */
  cleanUp() {
    this.extraHeader = {}
    this.extraParams = {}
  }

  /**
   * GET for contract only
   */
  get() {
    if (this.opts.showContractDesc) {
      this.extraParams = merge({}, this.extraParams, SHOW_CONTRACT_DESC_PARAM)
    }
    return this.request({}, {method: 'GET'}, this.contractHeader)
      .then(clientErrorsHandler)
      .then(result => {
        this.log('get contract result', result)
        // when refresh the window the result is different!
        // @TODO need to check the Koa side about why is that
        // also it should set a flag if we want the description or not
        if (result.cache && result.contract) {
          return result.contract;
        }
        // just the normal result
        return result
      })
  }

 /**
  * POST to server - query
  * @param {object} name of the resolver
  * @param {array} args arguments
  * @return {object} promise resolve to the resolver return
  */
 query(name, args = []) {
   return this.request(createQuery(name, args))
    .then(clientErrorsHandler)
 }

 /**
  * PUT to server - mutation
  * @param {string} name of resolver
  * @param {object} payload what it said
  * @param {object} conditions what it said
  * @return {object} promise resolve to the resolver return
  */
 mutation(name, payload = {}, conditions = {}) {
   return this.request(createMutation(name, payload, conditions), {method: PUT})
    .then(clientErrorsHandler)
 }

}
